Pi Sweeper

ECE 5725 Spring 2023
Juliet DeNapoli (jfd232), Changyuan Lin (cl859), Owen Louis (ocl6)


Demonstration Video


Introduction

This project is a physical rendition of the classic Minesweeper video game, using a hardware game board where squares can be uncovered by pressing them directly. There is an 8x8 grid, with each grid square illuminated from within by a colored RGB LED to display the nearby mine count, undiscovered status, flag, or location of mine when the game is lost. The player is able to push the grid squares down to explore that space, with other open squares automatically being revealed like how auto-exploration works in the traditional Minesweeper game. The Raspberry Pi and all hardware are enclosed in a custom 3D printed chassis, and the PiTFT displays a leaderboard with previous best times, filtered by difficulty.


Project

Project Objective:

  • Replicate Minesweeper in a physical form
  • Develop modular code that can be used for multiple NeoTrellis LED keypads
  • Create a 3D printed enclosure for our embedded system

Design

Software

The game was controlled using a Python program, with separate files for the leaderboard and saved game replay, containing the winning scores of the game and moves, seed, and difficulty for the most recently played game. Our game begins by randomly placing the mines throughout the board. The number of mines depends on the difficulty selected using the PiTFT buttons. 10 mines are placed at the default "easy" difficulty, but the player can choose to play medium (15 mines) or hard (20 mines). During initialization, the game computed the colors of all squares by looping through them and looking at the number of mines in adjacent grid squares. This saved computation time during gameplay, since the colors are known in advance but only revealed when the grid square is uncovered.

Once the board was generated, the game starts with all the squares lit up in white, to indicate everything as unrevealed. There were two types of presses, a long and short press. These were determined by recording the press time when buttons were pushed down in a dictionary, and calculating the duration when the button was released. A short press would reveal an unrevealed square, and depending on the sum of mines around that unrevealed square, the square would change color to indicate how many mines were around it. For one mine, it was blue, for two it was green, for three it was red, for four it was purple, and for five it was yellow. The square turns off if there are no adjacent mines. If the unrevealed square turned out to be a mine, revealing it would flash all the mines on the board red to indicate that the player lost, while also pausing the time and disabling any further interaction other than resetting the game.

However, if the player would have revealed a mine on the first try of the whole game, our board regenerated to a different board until it could be guaranteed that the given square was clear. This prevented the player from instantly losing, and thus made our game more enjoyable. We also implemented auto-exploration features like the original game, where if the player revealed a square with no adjacent mines, the adjacent squares were also progressively revealed until a colored number is revealed. This was done with breadth-first search, which provided a cool effect of the clear areas expanding from the touch point until they hit colored grid spaces.

A longer press flagged a square, and was indicated by a flashing white light. Once a square is flagged, it cannot be uncovered by a short press until it is unflagged by another long press. It is useful for the player to keep track of where all the mines are while playing. Flagging a square also decreased the remaining mine count in the segmented display on the top left of the system. If the player flags too many mines, this number turns negative, to indicate that some of the flagged squares the player thought were mines are not actually mines. The player cannot win until all safe squares are revealed.

The numeric display in the top right corner of our system displayed the time. If the player uncovered all the squares that didn't contain mines, they won, and their time was added to our leaderboard file along with the difficulty and date achieved. Our PiTFT displayed these best times, filtering through the leaderboard file to only display times from the current difficulty, sorted by lowest times. The first, second, and third place scores were displayed as gold, silver, and bronze, respectively. The leaderboard file persists across reboots and the Python program automatically starts when the system is turned on to provide for an embedded system.

After winning or losing a game, or if the player wanted to play a new game at any point, they could quick reset the game at the current difficulty using the big red button in the lower right corner. Alternatively, they could also reset the game with the difficulty buttons in case they wanted to play at a different mode. Resetting the game regenerated the board state with the corresponding number of mines, and reset the timer and flag states.

A new feature added after our final project demo, in preparation for static display, was the replay mode. This addition let the player record their played games into a log of actions taken, which was saved to a file, along with the game difficulty and game seed so the same game board can be regenerated later. Running the same program, the game log can be supplied as an additional argument, which disables interaction and has the Pi autonomously generate the game board and play it over and over again, with the same delays between button presses as the original human player. In replay mode, the new games played by the Pi were not recorded to the leaderboard, which still shows the regular leaderboard content.

Hardware

In order to display the typical grid of Minesweeper, we used four 4x4 button and LED combination circuit boards from Adafruit. These boards can display any RGB color, and the physical buttons are made of durable and transparent silicone. The boards are communicated with via I2C. Each board has five unique address changing soldering points that allowed us to use four in combination. This extra capacity would also allow us to further expand this project given a larger budget. These boards also have an interrupt pin that is used to trigger an interrupt service routine on the Raspberry Pi to ensure a rapid response to any button press.

Displaying the timer and mine counter was handled by two sets of four digit 14-segment display boards from Adafruit. These boards also communicated over I2C, and they had two address changing solder points. These displays were wired directly into the same I2C wires as the LED boards resulting in only five wires needing to be connected to the Raspberry Pi: 3V, GND, SDA, SCL, and one interrupt pin from the LED boards.

The arcade style button used a typical button microswitch given to us by Professor Skovira. This switch was wired to be a default low button in order to use the lowest amount of current.

The non-electronic hardware design stemmed largely from our initial concept drawing seen in Figure 1. This initial design sketch was made using rough measurements of all the components used, but it was merely an initial design idea. As we progressed, the design was segmented into distinct components due to the limitations of 3D printing bed sizes, and the design was also brought into three dimensions to resemble an arcade machine.

The first piece printed was the main body holding the four LED button grids and the two sets of 14-segment displays on the top. This piece took five drafts to get to be the perfect shape as the buttons needed just enough room to be pressed while also being structurally supportive. This component was also designed to house the 14-segment displays shown at the top of Figure 1. These displays needed to be perfectly flush with the front of the display while leaving room behind for their control boards and breakout pins. We also included two cylindrical mounting points components that can be seen in the bottom of Figure 2 that the piece holding the PiTFT and arcade style button would later mount to.

In order to retain the 4x4 LED matrix circuit boards together and in place, we had to design a custom solution. The circuit boards needed to be supported from the rear, but there was limited area from which they could be due to various resistors and capacitor placements. In order to fully support the LED circuit boards enough to withstand constant use from players, we used an I-beam style support that pressed against every corner of the circuit boards. We left gaps in the supports along each side of the circuit boards so that wires could be run from the 14-segment displays down to the Raspberry Pi.. We then used a small tab based alignment method to ensure that the support structure was in the correct position while gluing which can be seen in Figure 3. We also selected the I-beam style support due to its lower plastic volume than a more blocky style.

The bottommost part of the hardware design housed the Raspberry Pi, PiTFT, and arcade style button. The PiTFT was difficult to constrain in all directions for several reasons. The first was that the premade mounting holes are so small that 3D printed parts lack much strength at that size. The second is that the Raspberry Pi and PiTFT display are not exactly parallel, and this creates some difficult dimensions to measure. In order to get around the first issue we printed many drafts to find the exact maximum radius we could get away with, and we also used the port housing portion of the Raspberry Pi as an additional constraint. This cutout can be seen in the bottom right of Figure 4. In order to resolve the second issue, we used rubber foam sandwiched between the Raspberry Pi and the back of the housing to press the assembly against the front and thus constrain it depth-wise. We also had to make some small design modifications to accommodate some pins and pieces of solder on the PiTFT that can be seen in Figure 4. At the top of the figure below you can also see the cutout for the wiring between the two main bodies.

All remaining pieces of the final assembly were aesthetic only. This includes the “Pi Sweeper” name plate seen at the top of Figure 5 and the rear piece of the main body. We also printed our names as the structural supports of the two legs, and this can be partially seen below as well.

The arcade style button needed to be redesigned to fit within the tight depth constraints of its housing. The design was adapted from an open source design by Thingiverse user Wattage2308, and we only modified the inner radius of the outermost part to facilitate a more smooth button pressing motion. See the references section for the credited design and download link.


Drawings

Initial design
Figure 1: Initial Design
Main body
Figure 2: Main body showing holes for the buttons and two sets of 14-segment displays.
Rear view of main body
Figure 3: Rear view of the main body with the support structure for the four LED matrix circuit boards shown.
Rear view of PiTFT section
Figure 4: Rear view of the PiTFT and arcade style button housing showing the four mounting pins, cutout for the Raspberry Pi ports, and cutouts for the PiTFT buttons.
Front view of full assembly
Figure 5: Front view of the full assembly.
Preliminary testing
Figure 6: Preliminary testing.
Preliminary draft
Figure 7: Preliminary draft.

Testing

When we first received our parts, none of the NeoTrellis LED keypads were connected yet, so we began by testing our board generation on one 4x4 NeoTrellis board. This was a matter of creating a grid as our gameboard, randomly placing a few mines within this grid, and computing the surrounding mines for all the clear squares. This initial setup can be seen in Figure 6. When it came time to add more NeoTrellis boards, it was just a matter of changing the x and y dimensions of our game board, as well as adding the new NeoTrellis board to our array of boards with the right I2C addresses.

After this first draft, we were able to connect all the NeoTrellis boards, as well as connect the numeric displays. This can be seen in Figure 7. From here, we implemented board clearing, flagging, as well as the flag and time display on the numeric LEDs.

With all of this done, we added the bottom component of our system, which had the PiTFT and the reset button. We implemented how the reset button would implement the game, and how each of the different difficulty buttons on the PiTFT would reset the game with its respective. From there, the last part to complete was the leaderboard to be displayed on the PiTFT. After the final demo we also added the replay mode and tested it.


Results and Future Work

The final result of our project was full completion of our objective, and it ended up being a very fun game to play for everyone in the lab, so much that we had people coming over to play it. Every component that we purchased worked without problems, and integration was smooth due to the ease of I2C on the Raspberry Pi. We ended up slightly changing our chassis design as we went to create a better user experience when playing the game. The game looked and played as well as we could have hoped despite limitations of only having LEDs as the display.

In the future, we could expand upon our game to create an even bigger board. Luckily, with the way we developed our code to be modular, adding more boards wouldn't be difficult. As of right now, our hard difficulty is just a little too difficult, but that is only because it is on an 8x8 grid. In other versions of Minesweeper, different difficulties come with different sized boards.


Work Distribution

Changyuan

cl859@cornell.edu

  • Wrote, tested, and integrated the game software.
  • Worked on website.
  • Developed recorded gameplay program for future display.

Juliet

jfd232@cornell.edu

  • Wrote, tested, and integrated the game software.
  • Edited final video.
  • Worked on website.

Owen

ocl6@cornell.edu

  • Custom designed, tested, and iterated the 3D printed chassis and button.
  • Worked on website.

Parts List

Total: $124.70


References

Adafruit NeoTrellis Tutorial
Adafruit Alphanumeric LED Display Tutorial
R-Pi GPIO Document
Design that our button was based on
Button micro switch used

Code Appendix


        import time
        import board
        import random
        import RPi.GPIO as GPIO
        import pygame
        import os
        import sys
        from adafruit_neotrellis.neotrellis import NeoTrellis
        from adafruit_neotrellis.multitrellis import MultiTrellis
        from adafruit_ht16k33.segments import Seg14x4
        from bisect import bisect
        from collections import deque
        from datetime import datetime
        from pygame.locals import *  # For mouse variables
        
        # Size of the board
        X_DIM = 8
        Y_DIM = 8
        
        # Colors
        OFF = BLACK = (0, 0, 0)
        RED = (255, 0, 0)
        DARKRED = (127, 0, 0)
        ORANGE = (255, 150, 0)
        YELLOW = (255, 255, 0)
        GREEN = (0, 255, 0)
        CYAN = (0, 255, 255)
        BLUE = (0, 0, 255)
        PURPLE = (180, 0, 255)
        WHITE = (255, 255, 255)
        LESSWHITE = (40, 40, 40)
        SILVER = (150, 150, 150)
        
        # Tile colors, index by number of mines
        TILE_COLORS = [OFF, BLUE, GREEN, RED, PURPLE, ORANGE, CYAN]
        
        # Mines per difficulty (easy, medium, hard)
        DIFFICULTY_MINES = [10, 15, 20]
        
        # Leaderboard file
        LEADERBOARD_FILE = "leaderboard.csv"  # Relative to working directory
        
        # Actions log file
        ACTIONS_LOG_FILE = "game.log"
        
        # Declare the game variable
        game = None
        start_time = None
        difficulty = 0
        
        # In-memory leaderboard will be sorted by game duration
        leaderboard = []
        
        # Record actions
        actions_start_time = None
        actions_list = []
        
        # Replay mode (disables adding to leaderboard, buttons)
        replay_mode = False
        replay_difficulty = None
        replay_seed = None
        replay_list = []
        replay_index = 0
        replay_start_time = None
        
        # Enable replay mode if argument given
        if len(sys.argv) == 2:
            replay_mode = True
            with open(sys.argv[1], "r") as log:
                for line in log:
                    line = line.strip()
                    if line:
                        if line.startswith("difficulty"):
                            replay_difficulty = int(line.split(" ")[1])
                        elif line.startswith("seed"):
                            replay_seed = float(line.split(" ")[1])
                        else:
                            columns = line.split(",")
                            replay_list.append((int(columns[0]), int(columns[1]), int(columns[2]), float(columns[3])))
        
        code_run = True
        
        
        def quit_callback(channel):
            global code_run
            code_run = False
        
        
        class Tile:
            def __init__(self, x, y):
                self.number = 0
                self.flagged = False
                self.revealed = False
                self.x = x
                self.y = y
        
        
        class Minesweeper:
            def __init__(self, xdim, ydim, nmines):
                self.seed = time.time()  # Seed with system time and remember it
                if replay_mode:
                    self.seed = replay_seed
                print("Generating with seed", self.seed)
                random.seed(self.seed)
                self.xdim = xdim
                self.ydim = ydim
                self.nmines = nmines
                self.board = [[Tile(x, y) for x in range(xdim)] for y in range(ydim)]
                self.place_mines(nmines)
                self.compute_numbers()
                self.lost = False
                self.stop_elapsed = None
        
            def place_mines(self, nmines):
                if nmines > self.xdim * self.ydim:
                    print("Too many mines!!")
                imine = 0
                while imine < nmines:
                    x = random.randrange(0, self.xdim)
                    y = random.randrange(0, self.ydim)
                    if self.board[y][x].number != -1:
                        self.board[y][x].number = -1
                        imine += 1
        
            def compute_numbers(self):
                for y in range(self.ydim):
                    for x in range(self.xdim):
                        if self.board[y][x].number == -1:
                            continue
                        nearby = 0
                        for dy in range(-1, 2):
                            for dx in range(-1, 2):
                                if (
                                    0 <= y + dy < self.ydim
                                    and 0 <= x + dx < self.xdim
                                    and self.board[y + dy][x + dx].number == -1
                                ):
                                    nearby += 1
                        self.board[y][x].number = nearby
        
            def debug_print(self):
                print("-" * self.xdim)
                for y in range(self.ydim):
                    for x in range(self.xdim):
                        if self.board[y][x].number == -1:
                            print("X", end="")
                        elif self.board[y][x].number == 0:
                            print(" ", end="")
                        else:
                            print(self.board[y][x].number, end="")
                    print()
                print("-" * self.xdim)
        
            def reveal_square(self, x, y):
                tiles = deque([self.board[y][x]])
                seen = deque([self.board[y][x]])
                while tiles:
                    tile = tiles.popleft()
                    # If touched a mine
                    if tile.number == -1:
                        self.lost = True
                        return
                    # Else touched a safe square
                    else:
                        tile.revealed = True
                        trellis.color(tile.x, tile.y, TILE_COLORS[tile.number])
                        # Add nearby tiles to reveal if it was a 0
                        if tile.number == 0:
                            for dy in range(-1, 2):
                                for dx in range(-1, 2):
                                    if (
                                        0 <= tile.y + dy < self.ydim
                                        and 0 <= tile.x + dx < self.xdim
                                        and self.board[tile.y + dy][tile.x + dx] not in seen
                                        and not self.board[tile.y + dy][tile.x + dx].flagged
                                    ):
                                        tiles.append(self.board[tile.y + dy][tile.x + dx])
                                        seen.append(tile)
        
            def is_loss(self):
                return self.lost
        
            def is_win(self):
                clear = True
                for y in range(self.ydim):
                    for x in range(self.xdim):
                        if not self.board[y][x].revealed and self.board[y][x].number != -1:
                            clear = False
                return clear
        
            def color_flagged(self, color):
                for y in range(self.ydim):
                    for x in range(self.xdim):
                        if self.board[y][x].flagged:
                            trellis.color(x, y, color)
        
            def color_mines(self, color):
                for y in range(self.ydim):
                    for x in range(self.xdim):
                        if self.board[y][x].number == -1:
                            trellis.color(x, y, color)
        
            def num_flagged(self):
                num = 0
                for y in range(self.ydim):
                    for x in range(self.xdim):
                        if self.board[y][x].flagged:
                            num += 1
                return num
        
        
        def reset_game(difficulty):
            global game
            global start_time
            game = Minesweeper(X_DIM, Y_DIM, DIFFICULTY_MINES[difficulty])
            game.debug_print()
            start_time = None
            segment_mine.fill(0)
            segment_mine.print(game.nmines - game.num_flagged())
            segment_time.fill(0)
            segment_time.print(0)
            for y in range(Y_DIM):
                for x in range(X_DIM):
                    # color white by default
                    trellis.color(x, y, WHITE)
        
        
        def load_leaderboard():
            try:
                with open(LEADERBOARD_FILE, "r") as leaderboard_file:
                    for line in leaderboard_file:
                        line = line.strip()
                        if line:
                            columns = line.split(",")
                            leaderboard.append(
                                (
                                    datetime.fromisoformat(columns[0]),
                                    int(columns[1]),
                                    float(columns[2]),
                                )
                            )
                leaderboard.sort(key=lambda tup: tup[2])
            except FileNotFoundError:
                print("No leaderboard file found", LEADERBOARD_FILE)
        
        
        def append_leaderboard(timestamp, difficulty, gametime):
            with open(LEADERBOARD_FILE, "a") as leaderboard_file:
                leaderboard_file.write(f"{timestamp},{difficulty},{gametime}\n")
            ranking = bisect([tup[2] for tup in leaderboard], gametime)
            leaderboard.insert(ranking, (timestamp, difficulty, gametime))
            return ranking
        
        
        # Hardware setup
        
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(26, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        if not replay_mode:
            GPIO.add_event_detect(
                26, GPIO.FALLING, callback=lambda channel: reset_game(difficulty), bouncetime=300
            )
        GPIO.setup(27, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.add_event_detect(27, GPIO.FALLING, callback=quit_callback, bouncetime=300)
        GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.setup(22, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        
        # Create the i2c object for the trellis
        i2c_bus = board.I2C()  # uses board.SCL and board.SDA
        # i2c_bus = board.STEMMA_I2C()  # For using the built-in STEMMA QT connector on a microcontroller
        
        # Create the trellis
        trelli = [
            [NeoTrellis(i2c_bus, False, addr=0x2E), NeoTrellis(i2c_bus, False, addr=0x2F)],
            [NeoTrellis(i2c_bus, False, addr=0x30), NeoTrellis(i2c_bus, False, addr=0x31)],
        ]
        trellis = MultiTrellis(trelli)
        
        # Set the brightness value (0 to 1.0)
        trellis.brightness = 0.5
        
        # Create a 14 segment display
        segment_mine = Seg14x4(i2c_bus, address=0x71)
        segment_time = Seg14x4(i2c_bus, address=0x70)
        
        # Button held status
        buttons_held = {}
        # Flagged flash time
        flash_time = time.time()
        flash_on = False
        # Variable to only refresh timer when needed
        last_shown_time = 0
        
        
        # Record actions
        def record_action(xcoord, ycoord, edge):
            global actions_start_time
            global actions_list
            if not game.stop_elapsed:
                if not actions_start_time:
                    actions_start_time = time.time()
                    actions_list = [(xcoord, ycoord, edge, 0)]
                else:
                    actions_list.append((xcoord, ycoord, edge, time.time() - actions_start_time))
        
        
        # Finish recording of a game
        def record_end():
            global actions_start_time
            global actions_list
            print(actions_list)
            with open(ACTIONS_LOG_FILE, "w") as log_file:
                log_file.write(f"difficulty {difficulty}\n")
                log_file.write(f"seed {game.seed}\n")
                for action in actions_list:
                    log_file.write(f"{action[0]},{action[1]},{action[2]},{action[3]}\n")
            actions_start_time = None
            actions_list = []
        
        
        # Replay simulates button presses
        def replay_update():
            global replay_index
            global replay_start_time
            if not replay_mode:
                print("Replay mode not enabled!")
                return
            if not replay_start_time:
                replay_start_time = time.time()
            replay_elapsed = time.time() - replay_start_time
            while replay_index < len(replay_list) and replay_list[replay_index][3] <= replay_elapsed:
                xcoord, ycoord, edge, _ = replay_list[replay_index]
                press(xcoord, ycoord, edge)
                replay_index += 1
            # If at the end, wait for a while and then reset
            if replay_index == len(replay_list) and replay_list[replay_index - 1][3] + 5 <= replay_elapsed:
                reset_game(replay_difficulty)
                replay_index = 0
                replay_start_time = None
        
        
        # Callback for button events
        def press(xcoord, ycoord, edge):
            global start_time
            global game
            if not replay_mode:
                record_action(xcoord, ycoord, edge)
            if edge == NeoTrellis.EDGE_RISING:
                # When pressing down, save the current time
                buttons_held[(xcoord, ycoord)] = time.time()
            elif edge == NeoTrellis.EDGE_FALLING:
                # Safety check that the key exists -- sometimes this causes an error
                if (xcoord, ycoord) not in buttons_held:
                    print(f"Problem with {xcoord}, {ycoord}, skipping")
                    return
                # Get how long the button was pressed
                duration = time.time() - buttons_held[(xcoord, ycoord)]
                del buttons_held[(xcoord, ycoord)]
                # On first press make sure it's not a mine (regen board)
                while not start_time and game.board[ycoord][xcoord].number == -1:
                    game = Minesweeper(X_DIM, Y_DIM, DIFFICULTY_MINES[difficulty])
                # Only process presses if game hasn't ended and not revealed
                if not game.board[ycoord][xcoord].revealed and not game.stop_elapsed:
                    # Flag/unflag if held long enough
                    if duration >= 0.3:
                        if game.board[ycoord][xcoord].flagged:
                            print(f"Unflag {xcoord}, {ycoord}")
                            game.board[ycoord][xcoord].flagged = False
                            trellis.color(xcoord, ycoord, WHITE)
                        else:
                            print(f"Flag {xcoord}, {ycoord}")
                            game.board[ycoord][xcoord].flagged = True
                        # Update segment display with number of mines remaining
                        segment_mine.fill(0)
                        segment_mine.print(game.nmines - game.num_flagged())
                    # Reveal mine and check game status on regular press
                    elif not game.board[ycoord][xcoord].flagged:
                        print(f"Reveal {xcoord}, {ycoord}")
                        game.reveal_square(xcoord, ycoord)
                        if not start_time:
                            start_time = time.time()
                        if game.is_loss():
                            print("YOU LOSE")
                            game.stop_elapsed = time.time() - start_time
                            segment_time.fill(0)
                            segment_time.print(int(game.stop_elapsed))
                            segment_mine.fill(0)
                            segment_mine.print("LOSE")
                            if not replay_mode:
                                record_end()
                        elif game.is_win():
                            print("YOU WIN")
                            game.stop_elapsed = time.time() - start_time
                            if not replay_mode:
                                ranking = append_leaderboard(
                                    datetime.now(), difficulty, game.stop_elapsed
                                )
                                print("Ranking", ranking)
                                draw_home()
                            segment_time.fill(0)
                            segment_time.print(int(game.stop_elapsed))
                            segment_mine.fill(0)
                            segment_mine.print("WIN")
                            if not replay_mode:
                                record_end()
        
        
        # Set up key callbacks
        if not replay_mode:
            for y in range(Y_DIM):
                for x in range(X_DIM):
                    # activate rising edge events on all keys
                    trellis.activate_key(x, y, NeoTrellis.EDGE_RISING)
                    # activate falling edge events on all keys
                    trellis.activate_key(x, y, NeoTrellis.EDGE_FALLING)
                    # set all keys to trigger the blink callback
                    trellis.set_callback(x, y, press)
                    # color white by default
                    trellis.color(x, y, RED)
        
        
        # Handler for difficulty button presses (callbacks not all working)
        def handle_difficulty_buttons():
            global difficulty
            if not GPIO.input(17):
                difficulty = 0
                draw_home()
                reset_game(difficulty)
            elif not GPIO.input(22):
                difficulty = 1
                draw_home()
                reset_game(difficulty)
            elif not GPIO.input(23):
                difficulty = 2
                draw_home()
                reset_game(difficulty)
        
        
        # PyGame setup
        
        os.putenv("SDL_VIDEODRIVER", "fbcon")  # Display on piTFT
        os.putenv("SDL_FBDEV", "/dev/fb0")
        # os.putenv('SDL_MOUSEDRV', 'TSLIB') # Track mouse clicks on piTFT
        # os.putenv('SDL_MOUSEDEV', '/dev/input/touchscreen')
        
        pygame.init()
        pygame.mouse.set_visible(False)
        screen = pygame.display.set_mode((320, 240))
        screen.fill(BLACK)
        font_big = pygame.font.Font(None, 32)
        font_small = pygame.font.Font(None, 24)
        
        
        def draw_add_leaderboard():
            date_header = font_big.render("Date", True, WHITE)
            time_header = font_big.render("Time", True, WHITE)
            screen.blit(date_header, (30, 10))
            screen.blit(time_header, (180, 10))
            vertical_spacing = 24
            leaderboard_difficulty = [tup for tup in leaderboard if tup[1] == difficulty]
            for i, (date, _, time) in enumerate(leaderboard_difficulty):
                if i == 0:
                    color = YELLOW
                elif i == 1:
                    color = SILVER
                elif i == 2:
                    color = ORANGE
                else:
                    color = WHITE
                rank_text = font_small.render(str(i), True, color)
                date_text = font_small.render(date.isoformat(" ", "minutes"), True, color)
                time_text = font_small.render(str(round(time, 3)), True, color)
                screen.blit(rank_text, (10, 40 + vertical_spacing * i))
                screen.blit(date_text, (30, 40 + vertical_spacing * i))
                screen.blit(time_text, (180, 40 + vertical_spacing * i))
        
        
        def draw_add_difficulties():
            difficulties = [("EASY", BLUE), ("MED", GREEN), ("HARD", RED)]
            for i, (name, color) in enumerate(difficulties):
                selected = i == difficulty
                rect = pygame.Rect(
                    280 if selected else 290, 20 + 60 * i, 40 if selected else 30, 60
                )
                screen.fill(color, rect)
                text = pygame.transform.rotate(font_small.render(name, True, WHITE), 90)
                screen.blit(text, text.get_rect(center=rect.center))
        
        
        def draw_home():
            screen.fill(BLACK)
            draw_add_leaderboard()
            draw_add_difficulties()
            pygame.display.flip()
        
        
        # Initialize the leaderboard
        load_leaderboard()
        print(leaderboard)
        
        # Reset the game for first run
        reset_game(0 if not replay_mode else replay_difficulty)  # Easy by default
        draw_home()
        
        # Main game loop
        while code_run:
            # call the sync function call any triggered callbacks
            trellis.sync()
            # Flash flagged squares
            game.color_flagged(WHITE if flash_on else LESSWHITE)
            # Flash mines if game lost
            if game.is_loss():
                game.color_mines(WHITE if flash_on else RED)
            # Update flash state
            if time.time() - flash_time >= 0.5:
                flash_on = not flash_on
                flash_time = time.time()
            # Update timer display
            if start_time and not game.stop_elapsed:
                elapsed = int(time.time() - start_time)
                if elapsed != last_shown_time:
                    segment_time.fill(0)
                    segment_time.print(elapsed)
                    last_shown_time = elapsed
            # Handle playback if replay mode
            if replay_mode:
                replay_update()
            # Otherwise check polling buttons as normal
            else:
                # Check for difficulty button presses
                handle_difficulty_buttons()
            # the trellis can only be read every 17 millisecons or so
            time.sleep(0.02)
        
        GPIO.cleanup()